﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;

namespace Sworm
{
    [System.Diagnostics.DebuggerDisplay("{Name} on {DeclaringType}")]
    public sealed class Member
    {
        private readonly PropertyInfo property;
        private readonly FieldInfo field;

        public Member(PropertyInfo property)
        {
            this.property = property;
            this.field = null;
        }

        public Member(FieldInfo field)
        {
            this.field = field;
            this.property = null;
        }

        public bool IsProperty { get { return property != null; } }
        public bool IsField { get { return field != null; } }

        public Type Type
        {
            get
            {
                if (property != null)
                {
                    return property.PropertyType;
                }
                return field.FieldType;
            }
        }

        public string Name { get { return AsMemberInfo.Name; } }

        public Type DeclaringType { get { return AsMemberInfo.DeclaringType; } }

        public bool IsPubliclyWritable
        {
            get
            {
                if (property != null)
                {
                    return property.CanWrite;
                }
                return field.IsPublic && !field.IsInitOnly;
            }
        }

        public bool IsPubliclyReadable
        {
            get
            {
                if (property != null)
                {
                    return property.CanRead;
                }
                return field.IsPublic;
            }
        }

        public IEnumerable<Attribute> GetCustomAttributes(bool inherit)
        {
            return AsMemberInfo.GetCustomAttributes(inherit).OfType<Attribute>();
        }

        static List<MethodInfo> cellAccessMethods = typeof(DbAccessor)
            .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
            .ToList();

        private Action<object, DbAccessor> dbSetter;
        private Action<object, DbAccessor> BuildDbSetter()
        {
            if (dbSetter != null)
            {
                return dbSetter;
            }

            var method = cellAccessMethods.SingleOrDefault(m => m.ReturnType == this.Type);
            if (method == null)
            {
                return null;
            }

            var instance = Expression.Parameter(typeof(object));
            var accessor = Expression.Parameter(typeof(DbAccessor));
            var convertInstance = Expression.Convert(instance, this.DeclaringType);
            var property = Expression.PropertyOrField(convertInstance, this.Name);
            var callMethod = Expression.Call(accessor, method);
            var assign = Expression.Assign(property, callMethod);

            var lambda = Expression.Lambda<Action<object, DbAccessor>>(assign, instance, accessor);
            dbSetter = lambda.Compile();
            return dbSetter;
        }

        public bool TrySetDbResult(object instance, DbAccessor accessor)
        {
            var s = BuildDbSetter();
            if (s == null)
            {
                return false;
            }
            s(instance, accessor);
            return true;
        }

        public void SetValue(object instance, object value)
        {
            var setter = BuildSetter();
            try
            {
                setter(instance, value);
            }
            catch (InvalidCastException ex)
            {
                string valueType = value == null ? "[null]" : value.GetType().FullName;

                var message = string.Format("cannot set {0}.{1} to {2} - {3}",
                    this.DeclaringType.FullName, this.Name, value, valueType);

                throw new InvalidDbCastException(message, ex);
            }
        }

        private Action<object, object> setter;
        private Action<object, object> BuildSetter()
        {
            if (setter != null)
            {
                return setter;
            }

            var instance = Expression.Parameter(typeof(object));
            var value = Expression.Parameter(typeof(object));
            var convertInstance = Expression.Convert(instance, this.DeclaringType);
            var convertValue = Expression.Convert(value, this.Type);
            var member = Expression.PropertyOrField(convertInstance, this.Name);
            var assign = Expression.Assign(member, convertValue);

            var lambda = Expression.Lambda<Action<object, object>>(assign, instance, value);
            setter = lambda.Compile();
            return setter;
        }

        private Func<object, object> getter;
        private Func<object, object> BuildGetter()
        {
            if (getter != null)
            {
                return getter;
            }

            var instance = Expression.Parameter(typeof(object));
            var convertInstance = Expression.Convert(instance, this.DeclaringType);
            var member = Expression.PropertyOrField(convertInstance, this.Name);
            var convertMember = Expression.Convert(member, typeof(object));

            var lambda = Expression.Lambda<Func<object, object>>(convertMember, instance);
            getter = lambda.Compile();
            return getter;
        }

        public object GetValue(object instance)
        {
            return BuildGetter()(instance);
        }

        private Action<object> ensureCollectionInitialized;
        private Action<object> BuildEnsureCollectionInitialized()
        {
            if (ensureCollectionInitialized != null)
            {
                return ensureCollectionInitialized;
            }

            var instance = Expression.Parameter(typeof(object));
            var convertInstance = Expression.Convert(instance, this.DeclaringType);
            var collection = Expression.PropertyOrField(convertInstance, this.Name);

            var testNull = Expression.Equal(collection, Expression.Constant(null));

            var callConstructor = Expression.New(this.Type); // TODO this won't handle IList<T>, etc.
            var assign = Expression.Assign(collection, callConstructor);

            var ifStatement = Expression.IfThen(testNull, assign);

            var lambda = Expression.Lambda<Action<object>>(ifStatement, instance);
            ensureCollectionInitialized = lambda.Compile();
            return ensureCollectionInitialized;
        }

        public void EnsureCollectionInitialized(object collectionOwner)
        {
            BuildEnsureCollectionInitialized()(collectionOwner);
        }

        public void AddToCollection(object collectionOwner, object itemToAdd)
        {
            BuildEnsureCollectionInitialized()(collectionOwner);
            BuildCollectionAdder()(collectionOwner, itemToAdd);
        }

        private Action<object, object> collectionAdder;
        private Action<object, object> BuildCollectionAdder()
        {
            if (collectionAdder != null)
            {
                return collectionAdder;
            }

            var item = Expression.Parameter(typeof(object));
            var convertItem = Expression.Convert(item, CollectionOfType);

            var instance = Expression.Parameter(typeof(object));
            var convertInstance = Expression.Convert(instance, this.DeclaringType);
            var collection = Expression.PropertyOrField(convertInstance, this.Name);
            var method = this.Type.GetMethod("Add");
            var add = Expression.Call(collection, method, convertItem);

            var lambda = Expression.Lambda<Action<object, object>>(add, instance, item);
            collectionAdder = lambda.Compile();
            return collectionAdder;
        }

        public bool IsCollection
        {
            get
            {
                return typeof(System.Collections.IEnumerable).IsAssignableFrom(Type)
                    && Type != typeof(string) && Type != typeof(byte[]);
            }
        }

        public Type CollectionOfType
        {
            get { return Helpers.UndoEnumerable(Type); }
        }



        public MemberInfo AsMemberInfo
        {
            get { return (property as MemberInfo ?? field); }
        }

        public PropertyInfo AsPropertyInfo
        {
            get
            {
                if (property == null)
                {
                    throw new InvalidOperationException("This Member is not a property");
                }
                return property;
            }
        }

        public FieldInfo AsFieldInfo
        {
            get
            {
                if (field == null)
                {
                    throw new InvalidOperationException("This Member is not a field");
                }
                return field;
            }
        }

        public override int GetHashCode()
        {
            return AsMemberInfo.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            var other = obj as Member;
            if (other == null)
            {
                return false;
            }
            return AsMemberInfo.Equals(other.AsMemberInfo);
        }

        public static bool operator ==(Member first, Member second)
        {
            if (null == (object)first && null == (object)second)
            {
                return true;
            }
            if (null == first || null == second)
            {
                return false;
            }
            return first.AsMemberInfo.Equals(second.AsMemberInfo);
        }

        public static bool operator !=(Member first, Member second)
        {
            return !(first == second);
        }
    }
}
